1 module commons;
2 public import arsd.terminal : Color, ConsoleOutputType, ConsoleInputFlags;
3 public static import arsd.terminal;
4 public import std.array:join, split;
5 public import std.json;
6 public import std.path;
7 public import std.process;
8 public static import std.file;
9 public import default_handlers;
10 
11 
12 enum hipremeEngineRepo = "https://github.com/MrcSnm/HipremeEngine.git";
13 enum ConfigFile = "gamebuild.json";
14 
15 JSONValue engineConfig;
16 Config configs;
17 
18 string pathBeforeNewLdc;
19 
20 struct Terminal
21 {
22 	import std.stdio;
23 	arsd.terminal.Terminal* arsdTerminal;
24 	this(arsd.terminal.Terminal* arsdTerminal)
25 	{
26 		this.arsdTerminal = arsdTerminal;
27 	}
28 
29 	void color(Color main, Color secondary){if(arsdTerminal) arsdTerminal.color(main, secondary);}
30 	int cursorY()
31 	{
32 		if(arsdTerminal) return arsdTerminal.cursorY;
33 		return 0;
34 	}
35 	string getline(string message)
36 	{
37 		if(arsdTerminal) return arsdTerminal.getline(message); 
38 		std.stdio.writeln("Can't get line with message [", message, "]");
39 		return "";
40 	}
41 	void moveTo(int x, int y){if(arsdTerminal) arsdTerminal.moveTo(x, y);}
42 	void clear(){if(arsdTerminal) arsdTerminal.clear();}
43 	void write(T...)(T args)
44 	{
45 		if(arsdTerminal) arsdTerminal.write(args);
46 		else std.stdio.write(args);
47 	}
48 	void flush()
49 	{
50 		if(arsdTerminal) arsdTerminal.flush();
51 	}
52 	void hideCursor(){ if(arsdTerminal) arsdTerminal.hideCursor();}
53 	void showCursor(){ if(arsdTerminal) arsdTerminal.showCursor();}
54 	void clearToEndOfLine(){ if(arsdTerminal) arsdTerminal.clearToEndOfLine();}
55 
56 	void writeln(T...)(T args)
57 	{
58 		if (arsdTerminal) arsdTerminal.writeln(args);
59 		else std.stdio.writeln(args);
60 	}
61 	~this()
62 	{
63 		if(arsdTerminal) destroy(*arsdTerminal);
64 	}
65 }
66 
67 struct RealTimeConsoleInput
68 {
69 	private arsd.terminal.RealTimeConsoleInput* input;
70 	this(arsd.terminal.RealTimeConsoleInput* input){this.input = input;}
71 	dchar getch()
72 	{
73 		if(input) return input.getch();
74 		return '\0';
75 	}
76 	~this()
77 	{
78 		if(input) destroy(*input);
79 	}
80 }
81 
82 struct TerminalColors
83 {
84 	private Terminal* _t;
85 	this(Color main, Color secondary, ref Terminal terminal)
86 	{
87 		_t = &terminal;
88 		_t.color(main, secondary);
89 	}
90 	~this()
91 	{
92 		_t.color(Color.DEFAULT, Color.DEFAULT);
93 	}
94 }
95 
96 struct WorkingDir
97 {
98 	private string _currDir;
99 	this(string targetDir)
100 	{
101 		_currDir = std.file.getcwd();
102 		std.file.chdir(targetDir);
103 	}
104 	~this(){std.file.chdir(_currDir);}
105 }
106 
107 enum ChoiceResult
108 {
109 	None,
110 	Continue,
111 	Error,
112 	Back,
113 }
114 
115 struct Choice
116 {
117 	string name;
118 	ChoiceResult function(Choice* self, ref Terminal t, ref RealTimeConsoleInput input, in CompilationOptions opts) onSelected;
119 	bool shouldTime;
120 	string function() updateChoice;
121 	bool scriptOnly;
122 
123 	this(string name,
124 	ChoiceResult function(Choice* self, ref Terminal t, ref RealTimeConsoleInput input, in CompilationOptions opts) onSelected,
125 	bool shouldTime = false,
126 	string function() updateChoice = null, bool scriptOnly = false)
127 	{
128 		this.name = updateChoice ? updateChoice() : name;
129 		this.onSelected = onSelected;
130 		this.shouldTime = shouldTime;
131 		this.updateChoice = updateChoice;
132 		this.scriptOnly = scriptOnly;
133 	}
134 
135 	bool opEquals(ref const Choice other) const 
136 	{
137 		return name == other.name;
138 	}
139 	bool opEquals(string choiceName) const
140 	{
141 		return name == choiceName;	
142 	}
143 }
144 
145 struct Config
146 {
147 	JSONValue cfg;
148 
149 	this(JSONValue js)
150 	{
151 		cfg = js;
152 		if(!("windows" in cfg)) cfg.object["windows"] = JSONValue(string[string].init);
153 		if(!("posix" in cfg)) cfg["posix"] = JSONValue(string[string].init);
154 	}
155 	string toString()
156 	{
157 		return cfg.toPrettyString(JSONOptions.doNotEscapeSlashes);
158 	}
159 
160 	auto opBinaryRight(string op, R)(const R rhs) const
161 	if(op == "in")
162 	{
163 		version(Windows){return rhs in cfg["windows"];}
164 		else version(Posix){return rhs in cfg["posix"];}
165 		else static assert(false, "OS not supported");
166 	}
167 
168 	ref auto opIndexAssign(T)(T value, string obj)
169 	{
170 		version(Windows){return cfg["windows"][obj] = value;}
171 		else version(Posix){return cfg["posix"][obj] = value;}
172 		else static assert(false, "OS not supported");
173 	}
174 
175 	ref auto opIndex(string obj)
176 	{
177 		version(Windows){return cfg["windows"][obj];}
178 		else version(Posix){return cfg["posix"][obj];}
179 		else static assert(false, "OS not supported");
180 	}
181 }
182 
183 struct CompilationOptions
184 {
185 	bool skipRegistry;
186 	bool dubVerbose;
187 	bool force;
188 	bool tempBuild;
189 	string getDubOptions() const
190 	{
191 		string ret;
192 		if(force) ret~= " --force";
193 		if(skipRegistry) ret~= " --skip-registry=all";
194 		if(tempBuild) ret~= " --temp-build";
195 		if(dubVerbose) ret~= " --verbose";
196 		return ret;
197 	}
198 }
199 
200 T[] unique(T)(T[] input)
201 {
202 	bool[T] seen;
203 	T[] ret;
204 	foreach(v; input)
205 	{
206 		if(!(v in seen))
207 		{
208 			seen[v] = true;
209 			ret~= v;
210 		}
211 	}
212 	return ret;
213 }
214 
215 size_t selectChoiceBase(ref Terminal terminal, ref RealTimeConsoleInput input, Choice[] choices, 
216 	string selectionTitle, size_t selectedChoice = 0)
217 {
218 	bool exit;
219 	enum ArrowUp = 983078;
220 	enum ArrowDown = 983080;
221 	enum SelectionHint = "Select an option by using W/S or Arrow Up/Down and choose it by pressing Enter.";
222 	terminal.clear();
223 	terminal.color(Color.DEFAULT, Color.DEFAULT);
224 	terminal.writelnHighlighted(selectionTitle);
225 	terminal.writeln(SelectionHint);
226 
227 	static void changeChoice(ref Terminal t, Choice[] choices, string title, Choice current, Choice next, int nextCursorOffset)
228 	{
229 		int currCursor = t.cursorY;
230 		t.moveTo(0, currCursor);
231 		t.clearToEndOfLine();
232 		t.write(current.name);
233 
234 		t.moveTo(0, currCursor+nextCursorOffset);
235 		t.clearToEndOfLine();
236 		with(TerminalColors(Color.green, Color.DEFAULT, t))
237 			t.write(">> ", next.name);
238 		t.flush;
239 	}
240 
241 	static void changeChoiceClear(ref Terminal t, Choice[] choices, string title, Choice current, Choice next, int nextCursorOffset)
242 	{
243 		t.color(Color.DEFAULT, Color.DEFAULT);
244 		t.clear();
245 		t.writelnHighlighted(title);
246 		t.writeln(SelectionHint);
247 		foreach(i, c; choices)
248 		{
249 			if(c.name == next.name) with(TerminalColors(Color.green, Color.DEFAULT, t))
250 				t.writeln(">> ", c.name);
251 			else t.writeln(c.name);
252 		}
253 		t.flush;
254 	}
255 
256 	int startLine = terminal.cursorY;
257 	terminal.color(Color.DEFAULT, Color.DEFAULT);
258 
259 	foreach(i, choice; choices)
260 		terminal.write(choice.name, i == choices.length - 1 ? "" : "\n");
261 	terminal.flush();
262 
263 	terminal.moveTo(0, startLine + cast(int)selectedChoice);
264 	terminal.hideCursor();
265 
266 
267 	size_t oldChoice = selectedChoice;
268 	while(!exit)
269 	{
270 		changeChoiceClear(terminal, choices, selectionTitle, choices[oldChoice], choices[selectedChoice], cast(int)(cast(long)selectedChoice-oldChoice));
271 		oldChoice = selectedChoice;
272 		CheckInput: switch(input.getch)
273 		{
274 			case 'w', 'W', ArrowUp:
275 				selectedChoice = (selectedChoice + choices.length - 1) % choices.length;
276 				break;
277 			case 's', 'S', ArrowDown:
278 				selectedChoice = (selectedChoice+1) % choices.length;
279 				break;
280 			case '\n':
281 				exit = true;
282 				break;
283 			default: goto CheckInput;
284 		}
285 	}
286 	terminal.moveTo(0, cast(int)startLine);
287 	foreach(i; 0..choices.length)
288 		terminal.moveTo(0, cast(int)(startLine+i)), terminal.clearToEndOfLine();
289 	terminal.moveTo(0, cast(int)startLine);
290 	terminal.writelnSuccess(">> ", choices[selectedChoice].name);
291 
292 	terminal.showCursor();
293 	return selectedChoice;
294 }
295 
296 string[] getProjectsAvailable()
297 {
298 	import std.array;
299 	import std.algorithm;
300 	if(!("projectsAvailable" in configs))
301 		return [];
302 	return (configs["projectsAvailable"].array.map!((JSONValue v) => v.str)).array;
303 }
304 
305 string getValidPath(ref Terminal t, string pathRequired)
306 {
307 	string path;
308 	while(true)
309 	{
310 		path = t.getline(pathRequired);
311 		if(std.file.exists(path))
312 			return path;
313 	}
314 }
315 
316 bool filesExists(string basePath, scope immutable string[] files...)
317 {
318 	foreach(f; files)
319 	{
320 		auto temp = buildNormalizedPath(basePath, f);
321 		if(!std.file.exists(temp)) return false;
322 	}
323 	return true;
324 }
325 
326 string getFirstExisting(string basePath, scope string[] tests...)
327 {
328 	foreach(t; tests)
329 	{
330 		auto temp = buildNormalizedPath(basePath, t);
331 		if(std.file.exists(temp)) return temp;
332 	}
333 	return "";
334 }
335 
336 string getHipPath(scope string[] paths...)
337 {
338 	return buildPath([configs["hipremeEnginePath"].str] ~ paths);
339 }
340 
341 string getFirstExistingVar(scope string[] vars...)
342 {
343 	foreach(variable; vars)
344 	{
345 		if(variable in environment)
346 			return environment[variable];
347 	}
348 	return "";
349 }
350 
351 
352 
353 bool hasLdc()
354 {
355 	return ("ldcPath" in configs) !is null;
356 }
357 
358 private bool dbgExecuteShell(scope const(char)[] command, ref Terminal t, const string[string] env = null)
359 {
360 	auto ret = executeShell(command, env);
361 	if(ret.status)
362 	{
363 		t.writelnError(cast(string)("Command '"~command~"' failed with: "~ ret.output));
364 		t.flush;
365 	}
366 	return ret.status == 0;
367 }
368 
369 string findProgramPath(string program)
370 {
371 	import std.algorithm:countUntil;
372 	import std.process;
373 	string searcher;
374 	version(Windows) searcher = "where";
375 	else version(Posix) searcher = "which";
376 	else static assert(false, "No searcher program found in this OS.");
377 	auto shellRes = executeShell(searcher ~" " ~ program,
378 	[
379 		"PATH": environment["PATH"]
380 	]);
381     if(shellRes.status == 0)
382 		return shellRes.output[0..shellRes.output.countUntil("\n")];
383    	return null;
384 }
385 
386 void writelnHighlighted(ref Terminal t, scope string[] what...)
387 {
388 	with(TerminalColors(Color.yellow, Color.DEFAULT, t))
389 		t.writeln(what.join());
390 }
391 
392 void writelnSuccess(ref Terminal t, scope string[] what...)
393 {
394 	with(TerminalColors(Color.green, Color.DEFAULT, t))
395 		t.writeln(what.join());
396 }
397 
398 void writelnError(ref Terminal t, scope string[] what...)
399 {
400 	with(TerminalColors(Color.red, Color.DEFAULT, t))
401 		t.writeln(what.join());
402 }
403 
404 auto timed(T)(scope T delegate() dg)
405 {
406 	import std.datetime.stopwatch;
407 	import std.stdio;
408 	StopWatch sw = StopWatch(AutoStart.yes);
409 	static if(is(T == void))
410 	{
411 		dg();
412 		writeln(sw.peek.total!"msecs", "ms");
413 	}
414 	else 
415 	{
416 		auto ret = dg();
417 		writeln(sw.peek.total!"msecs", "ms");
418 		return ret;
419 	}
420 }
421 auto timed(T)(ref Terminal t, scope T delegate() dg)
422 {
423 	import std.datetime.stopwatch;
424 	StopWatch sw = StopWatch(AutoStart.yes);
425 	static if(is(T == void))
426 	{
427 		dg();
428 		t.writeln(sw.peek.total!"msecs", "ms");
429 	}
430 	else 
431 	{
432 		auto ret = dg();
433 		t.writeln(sw.peek.total!"msecs", "ms");
434 		return ret;
435 	}
436 }
437 
438 
439 struct Session
440 {
441 	struct Cache
442 	{
443 		size_t line;
444 		string file;
445 	}
446 	bool[Cache] cache;
447 }
448 private __gshared Session session;
449 
450 void cached(scope void delegate() dg, string f = __FILE__, size_t l = __LINE__)
451 {
452 	if(!(Session.Cache(l, f) in session.cache))
453 	{
454 		session.cache[Session.Cache(l, f)] = true;
455 		dg();
456 	}
457 }
458 
459 /** 
460  * Clears all cache.
461  * This may be useful after a dub.template.json was already generated.
462  * Or for example, after changing the current game.
463  */
464 void clearCache()
465 {
466 	session.cache.clear;
467 }
468 
469 bool pollForExecutionPermission(ref Terminal t, ref RealTimeConsoleInput input, string operation)
470 {
471 	t.writelnHighlighted(operation~" [Y]es/[N]o");
472 	t.flush;
473 	while(true)
474 	{
475 		switch(input.getch)
476 		{
477 			case 'y', 'Y': return true;
478 			case 'n', 'N': return false;
479 			default: break;
480 		}
481 	}
482 }
483 
484 bool extractZipToFolder(string zipPath, string outputDirectory, ref Terminal t)
485 {
486 	import std.zip;
487 	ZipArchive zip = new ZipArchive(std.file.read(zipPath));
488 	if(!std.file.exists(outputDirectory))
489 	{
490 		t.writeln("Creating directory ", outputDirectory);
491 		t.flush;
492 		std.file.mkdirRecurse(outputDirectory);
493 	}
494 	foreach(fileName, archiveMember; zip.directory)
495 	{
496 		string outputFile = buildNormalizedPath(outputDirectory, fileName);
497 		if(!std.file.exists(outputFile))
498 		{
499 			if(archiveMember.expandedSize == 0)
500 				std.file.mkdirRecurse(outputFile);
501 			else
502 			{
503 				string currentDirName = outputFile;
504 				///For some reason on linux it thinks that .a files are directories
505 				t.writeln("Extracting ", fileName);
506 				t.flush;
507 				currentDirName = currentDirName.dirName;
508 				if(!std.file.exists(currentDirName))
509 					std.file.mkdirRecurse(currentDirName);
510 				std.file.write(outputFile, zip.expand(archiveMember));
511 			}
512 		}
513 	}
514 	return true;
515 }
516 
517 
518 bool extractToFolder(string zPath, string outputDirectory, ref Terminal t, ref RealTimeConsoleInput input)
519 {
520 	import std.path;
521 	switch(zPath.extension)
522 	{
523 		case ".gz", ".xz":
524 			version(Posix)
525 			{
526 				return extractTarGzToFolder(zPath, outputDirectory, t);
527 			}
528 			else assert(false, "No .tar.gz support on non Posix");
529 		case ".zip":
530 			return extractZipToFolder(zPath, outputDirectory, t);
531 		case ".7zip", ".7z":
532 			return extract7ZipToFolder(zPath, outputDirectory, t, input);
533 		default:
534 			t.writelnError("Could not detect compressed archive type for "~zPath);
535 			return false;
536 	}
537 }
538 
539 bool extract7ZipToFolder(string zPath, string outputDirectory, ref Terminal t, ref RealTimeConsoleInput input)
540 {
541 	if(!install7Zip("Extracting the file at"~zPath, t, input))	
542 	{
543 		t.writelnError("This operation requires a 7zip installation.");
544 		return false;
545 	}
546 	if(!std.file.exists(zPath)) 
547 	{
548 		t.writelnError("File ", zPath, " does not exists.");
549 		return false;
550 	}
551 	t.writeln("Extracting ", zPath, " to ", outputDirectory);
552 	t.flush;
553 
554 	string folderName = baseName(outputDirectory);
555 	outputDirectory = dirName(outputDirectory);
556 	if(!std.file.exists(outputDirectory))
557 		std.file.mkdirRecurse(outputDirectory);
558 
559 	with(WorkingDir(outputDirectory))
560 	{
561 		bool ret = dbgExecuteShell(configs["7zip"].str ~ " x -y "~zPath~" "~folderName, t);
562 		return ret;
563 	}
564 }
565 
566 version(Posix)
567 bool extractTarGzToFolder(string tarGzPath, string outputDirectory, ref Terminal t)
568 {
569 	if(!std.file.exists(tarGzPath))
570 	{
571 		t.writelnError("File ", tarGzPath, " does not exists.");
572 		return false;
573 	}
574 	t.writeln("Extracting ", tarGzPath, " to ", outputDirectory);
575 	t.flush;
576 	std.file.mkdirRecurse(outputDirectory.dirName);
577 	return dbgExecuteShell("tar -xf "~tarGzPath~" -C "~outputDirectory.dirName, t);
578 }
579 
580 bool isRecognizedExtension(string ext)
581 {
582 	switch(ext)
583 	{
584 		case ".7z", ".7zip", ".tar", ".xz", ".zf", ".bz", ".gz", ".zip": return true;
585 		default: return false;
586 	}
587 }
588 
589 /** 
590  * Removes the extension (while keeping numeric extensions such as dmd-2.105.0)
591  * Params:
592  *   input = Input to remove extension
593  * Returns: 
594  */
595 string removeExtension(string input)
596 {
597 	import std.string:isNumeric;
598 	string ext;
599 	while((ext = input.extension).length && ext.isRecognizedExtension)
600 		input = input.setExtension("");
601 	return input;
602 }
603 
604 /** 
605  * 
606  * Params:
607  *   purpose = A message for the user to understand what is happening
608  *   link = The link to file which will be downloaded to a temp dir
609  *   outputName = A file name with a compressed archive extension (e.g: .zip, .7z, .tar.xz)
610  *   outputDirectory = Where the file from outputName will be extracted
611  *   t = Terminal 
612  *   input = RealTimeInput
613  * Returns: 
614  */
615 bool installFileTo(string purpose, string link, string outputName,
616 string outputDirectory, ref Terminal t, ref RealTimeConsoleInput input)
617 {
618 	string downloadDir = buildNormalizedPath(std.file.tempDir, outputName);
619 	if(!downloadFileIfNotExists(purpose, link, downloadDir, t, input))
620 	{
621 		t.writelnError("Download failed");
622 		t.flush;
623 		return false;
624 	}
625 
626 
627 	outputName = outputName.removeExtension;
628 
629 	string installDir = buildNormalizedPath(outputDirectory, outputName);
630 	if(!extractToFolder(downloadDir, installDir, t, input))
631 	{
632 		t.writelnError("Could not extract ",downloadDir, " to ", installDir);
633 		return false;
634 	}
635 
636 	return true;
637 }
638 
639 bool makeFileExecutable(string filePath)
640 {
641 	version(Windows) return true;
642 	version(Posix)
643 	{
644 		if(!std.file.exists(filePath)) return false;
645 		import std.conv:octal;
646 		std.file.setAttributes(filePath, octal!700);
647 		return true;
648 	}
649 }
650 
651 bool downloadFileIfNotExists(
652 	string purpose, string link, string outputName,
653 	ref Terminal t, ref RealTimeConsoleInput input
654 )
655 {
656 	import std.net.curl;
657 	import std.conv:to;
658 	string theDir = dirName(outputName);
659 	if(!std.file.exists(theDir))
660 		std.file.mkdirRecurse(theDir);
661 	if(!std.file.exists(outputName))
662 	{
663 		if(!pollForExecutionPermission(t, input, "Your system will download a file: "~ purpose~"("~link~")"))
664 			return false;
665 		t.writelnHighlighted("Download started.");
666 		t.flush;
667 		size_t time = downloadWithProgressBar(t, link, outputName);
668 		t.writelnSuccess("\nDownload succeeded after ", time.to!string, " msecs!");
669 		t.flush;
670 	}
671 	return true;
672 }
673 
674 private void terminalProgressBar(ref Terminal t, float percentage, ubyte ticksCount = 32)
675 {
676 	assert(percentage <= 1.0 && percentage >= 0, "Invalid percentage.");
677 
678 	ubyte drawnTicks = cast(ubyte)(ticksCount*percentage);
679 	int line = t.cursorY;
680 	t.moveTo(0, line);
681 	t.clearToEndOfLine();
682 	t.write("<");
683 	foreach(int i; 0..ticksCount)
684 	{
685 		t.color(i < drawnTicks ? Color.green : Color.red, Color.DEFAULT);
686 		t.write(i < drawnTicks ? "=" : ".");
687 	}
688 	t.color(Color.DEFAULT, Color.DEFAULT);
689 	t.write("> (", percentage*100, "%)");
690 	t.flush();
691 }
692 
693 /**
694 *	Same as std.net.curl.download
695 *	Difference is that it shows a progress bar while downloading.
696 *	Returns the time needed to download.
697 */
698 size_t downloadWithProgressBar(ref Terminal t, string url, string saveToPath, size_t updateDelay = 125)
699 {
700 	import std.net.curl:HTTP;
701 	import core.time:dur;
702 	import std.datetime.stopwatch:StopWatch, AutoStart;
703 	import std.stdio : File;
704 	size_t received, contentLength;
705 	HTTP conn = HTTP();
706 	conn.url = url;
707 	static void writer(string path)
708 	{
709 		auto f = File(path, "wb");
710 		while(true)
711 		{
712 			immutable(ubyte)[] data = receiveOnly!(immutable(ubyte)[]);
713 			if(data.length == 0)
714 				break;
715 			f.rawWrite(data);
716 		}
717 		ownerTid.send(true);
718 	}
719 	auto writerTid = spawn(&writer, saveToPath);
720 	t.hideCursor();
721 	StopWatch sw = StopWatch(AutoStart.yes);
722 	size_t downloadTime;
723 	conn.onReceive = (ubyte[] data)
724 	{
725 		import std.conv:to;
726 		if(contentLength == 0)
727 			contentLength = conn.responseHeaders["content-length"].to!size_t;
728 		received+= data.length;
729 		if(sw.peek.total!"msecs" >= updateDelay || received == contentLength)
730 		{
731 			downloadTime+= sw.peek.total!"msecs";
732 			terminalProgressBar(t, cast(float)received/contentLength);
733 			sw.reset();
734 		}
735 		send(writerTid, data.idup);
736 		return data.length;
737 	};
738 	conn.perform();
739 	send(writerTid, (immutable(ubyte)[]).init);
740 	receiveTimeout(dur!"msecs"(1000), (bool){}); //Block until finish
741 	t.showCursor();
742 	return downloadTime; 
743 }
744 
745 
746 private string getConfigPath()
747 {
748 	import core.runtime;
749 	static string cfgPath;
750 	if(cfgPath == "")
751 		cfgPath = buildNormalizedPath(Runtime.args[0].dirName, ConfigFile);
752 	return cfgPath;
753 }
754 private string getEngineConfigPath()
755 {
756 	return getHipPath("bin" ,"desktop", "engine_opts.json");
757 }
758 void updateEngineFile()
759 {
760 	std.file.write(getEngineConfigPath, engineConfig.toPrettyString());
761 }
762 void updateConfigFile()
763 {
764 	std.file.write(getConfigPath, configs.toString());
765 }
766 string getSourceCodeEditor(string projectPath)
767 {
768 	if(!("sourceCodeEditor" in configs) || configs["sourceCodeEditor"].str.length == 0)
769 	{
770 		string out_Editor;
771 		if(getDefaultSourceEditor(buildNormalizedPath(projectPath, "source", "gamescript", "entry.d"), out_Editor))
772 			configs["sourceCodeEditor"] = out_Editor;
773 		else
774 			configs["sourceCodeEditor"] = "";
775 		updateConfigFile();
776 	}
777 
778 	return configs["sourceCodeEditor"].str;
779 }
780 
781 bool openSourceCodeEditor(string projectPath)
782 {
783 	string sourceEditor = getSourceCodeEditor(projectPath);
784 	if(!sourceEditor.length)
785 		return false;
786 
787 	return executeShell(sourceEditor.escapeShellCommand~" "~projectPath.escapeShellCommand).status == 0;
788 }
789 
790 
791 string getGitExec()
792 {
793 	if("git" in configs)
794 	{
795 		version(Windows) return buildNormalizedPath(configs["git"].str, "git.exe");
796 		else return buildNormalizedPath(configs["git"].str, "git");
797 	}
798 	return "git ";
799 }
800 
801 bool hasGit()
802 {
803 	if(findProgramPath("git")) return true;
804 	return ("git" in configs) != null;
805 }
806 
807 void loadSubmodules(ref Terminal t, ref RealTimeConsoleInput input)
808 {
809 	import std.process;
810 	if(!hasGit)
811 	{
812 		if(!installGit(t, input))
813 			throw new Error("Git wasn't found. Git is necessary for loading the engine submodules.");
814 	}
815 	t.writelnSuccess("Updating Git Submodules");
816 	t.flush;
817 
818 	executeShell("cd "~ configs["hipremeEnginePath"].str ~ " && " ~ getGitExec~" submodule update --init --recursive");
819 }
820 
821 private bool install7Zip(string purpose, ref Terminal t, ref RealTimeConsoleInput input)
822 {
823 	if(!("7zip" in configs))
824 	{
825 		version(Windows)
826 		{
827 			string _7zPath = findProgramPath("7z");
828 			if(!_7zPath)
829 			{
830 				if(!downloadFileIfNotExists("Needs 7zip for "~purpose, "https://www.7-zip.org/a/7zr.exe", 
831 					buildNormalizedPath(std.file.getcwd(), "7z.exe"), t, input
832 				))
833 					return false;
834 
835 				string outFolder = buildNormalizedPath(std.file.getcwd(), "buildtools");
836 				std.file.mkdirRecurse(outFolder);
837 				std.file.rename(buildNormalizedPath(std.file.getcwd(), "7z.exe"), buildNormalizedPath(outFolder, "7z.exe"));
838 				configs["7zip"] = buildNormalizedPath(outFolder, "7z.exe");
839 			}
840 			else
841 				configs["7zip"] = buildNormalizedPath(_7zPath);
842 			updateConfigFile();
843 		}
844 		else version(Posix)
845 		{
846 			configs["7zip"] = "7za";
847 			updateConfigFile();
848 		}
849 	}
850 	return true;
851 }
852 
853 
854 private string getGitDownloadLink()
855 {
856 	version(Windows) return "https://github.com/git-for-windows/git/releases/download/v2.40.1.windows.1/MinGit-2.40.1-64-bit.zip";
857 	else return "";
858 }
859 
860 
861 private ChoiceResult _backFn(Choice* c, ref Terminal t, ref RealTimeConsoleInput input, in CompilationOptions cOpts)
862 {
863 	return ChoiceResult.Back;
864 }
865 Choice getBackChoice()
866 {
867 	return Choice("Back", &_backFn);
868 }
869 
870 
871 bool installGit(ref Terminal t, ref RealTimeConsoleInput input)
872 {
873 	version(Windows)
874 	{
875 		if(!("git" in configs))
876 		{
877 			string gitPath = buildNormalizedPath(std.file.getcwd(), "buildtools", "git");
878 			if(!installFileTo("Download Git for getting HipremeEngine's source code.", getGitDownloadLink(), "git.zip",
879 			gitPath, t, input))
880 			{
881 				t.writelnError("Git installation failed");
882 				return false;
883 			}
884 			configs["git"] = buildNormalizedPath(gitPath, "cmd");
885 			updateConfigFile();
886 		}
887 		return true;
888 	}
889 	else version(Posix)
890 	{
891 		t.writelnError("Please install Git to use build_selector.");
892 		return false;
893 	}
894 }
895 
896 
897 void runEngineDScript(ref Terminal t, string script, scope string[] args...)
898 {
899 	import std.array;
900 	import std.datetime.stopwatch;
901 	StopWatch sw = StopWatch(AutoStart.yes);
902 	t.writeln("Executing engine script ", script, " with arguments ", args);
903 	t.flush;
904 	auto exec = executeShell(configs["rdmdPath"].str ~ " " ~ buildNormalizedPath(configs["hipremeEnginePath"].str, "tools", "build", script)~" " ~ args.join(" "), 
905 	environment.toAA);
906 	t.writeln("    Finished in ", sw.peek.total!"msecs", "ms");
907 	t.writeln(exec.output);
908 	t.flush;
909 	if(exec.status)
910 	{
911 		t.writelnError("Script ", script, " failed with: ", exec.output);
912 		t.flush;
913 		throw new Error("Failed on engine script");
914 	}
915 }
916 
917 string getDubPath()
918 {
919 	string dub = buildNormalizedPath(configs["dubPath"].str, "dub");
920 	version(Windows) dub = dub.setExtension("exe");
921 	return dub;
922 }
923 
924 private int execDubBase(ref Terminal t, in DubArguments dArgs)
925 {
926 	import std.conv:to;
927 	if(absolutePath(configs["hipremeEnginePath"].str) != absolutePath(std.file.getcwd()))
928 	if(std.file.exists("dub.template.json"))
929 	{
930 		import template_processor;
931 		string out_DubFile;
932 		auto res = processTemplate(std.file.getcwd(), configs["hipremeEnginePath"].str, out_DubFile);
933 		if(res != TemplateProcessorResult.success)
934 		{
935 			t.writelnError(res.to!string, ":", out_DubFile);
936 			return -1;
937 		}
938 		try std.file.write("dub.json", out_DubFile);
939 		catch(Exception e){
940 			t.writelnError("Could not write dub.json");
941 			return -1;
942 		}
943 	}
944 	return 0;
945 }
946 
947 
948 mixin template BuilderPattern(Struct)
949 {
950 	static foreach(mem; __traits(allMembers, Struct))
951 	{
952 		import std.traits:isFunction;
953 		static if(!isFunction!(__traits(getMember, Struct, mem)) && mem[0] == '_')
954 		{
955 			mixin(typeof(__traits(getMember, Struct, mem)), " ", mem[1..$], "() { return ", mem, ";}",
956 			Struct, " ", mem[1..$], "(", typeof(__traits(getMember, Struct, mem)), " arg )",
957 			"{this.",mem, " = arg; return this;}");
958 		}
959 	}
960 }
961 
962 
963 immutable string[] compilers = ["auto", "ldc2", "dmd"];
964 string getSelectedCompiler()
965 {
966 	const(JSONValue)* c = "selectedCompiler" in configs;
967 	if(!c) return "auto";
968 	return compilers[c.get!uint];
969 }
970 
971 
972 struct DubArguments
973 {
974 	string _command;
975 	string _configuration;
976 	CompilationOptions _opts;
977 	string _dir;
978 	string _preCommands;
979 	string _compiler = "auto";
980 	string _arch;
981 	string _build;
982 	string _recipe;
983 	string _runArgs;
984 	bool _confirmKey;
985 	bool _deep;
986 	bool _parallel = true;
987 
988 	mixin BuilderPattern!(DubArguments);
989 	
990 	string getDubRunCommand()
991 	{
992 		string dub = getDubPath();
993 		string a = command; ///Arguments
994 		if(compiler == "auto") 
995 		{
996 			compiler = arch ? "ldc2" : getSelectedCompiler();
997 			compiler = compiler == "auto" ? "" : compiler;
998 		}
999 
1000 		if(parallel)      a~= " --parallel";
1001 		if(recipe)        a~= " --recipe="~recipe;
1002 		if(build)         a~= " --build="~build;
1003 		if(arch)          a~= " --arch="~arch;
1004 		if(compiler != "")a~= " --compiler="~compiler;
1005 		if(deep)		  a~= " --deep";
1006 		if(configuration) a~= " -c "~configuration;
1007 		if(opts != CompilationOptions.init) a~= opts.getDubOptions();
1008 		if(runArgs)       a~= " -- "~runArgs;
1009 
1010 
1011 		version(Windows)
1012 		{
1013 			if(confirmKey) a~= " && pause";
1014 		}
1015 		else version(Posix)
1016 		{
1017 			if(confirmKey) a~= " && read -p \"Press any key to continue... \" -n1 -s";
1018 		}
1019 		
1020 
1021 		return preCommands~dub~" "~a;
1022 	}
1023 }
1024 
1025 int waitDub(ref Terminal t, DubArguments dArgs)
1026 {
1027 	///Detects the presence of a template file before executing.
1028 	if(execDubBase(t, dArgs) == -1) return -1;
1029 	string toExec = dArgs.getDubRunCommand();
1030 	t.writeln(toExec);
1031 	t.flush;
1032 	return wait(spawnShell(toExec));
1033 }
1034 
1035 int execDub(ref Terminal t, DubArguments dArgs)
1036 {
1037 	import std.string:lineSplitter;
1038 	if(execDubBase(t, dArgs) == -1) return -1;
1039 	string toExec = dArgs.getDubRunCommand();
1040 	t.writeln(toExec);
1041 	t.flush;
1042 	auto res = executeShell(toExec, null, std.process.Config.none, size_t.max, dArgs.dir);
1043 	foreach(l; res.output.lineSplitter) t.writeln("\t", l);
1044 	return res.status;
1045 }
1046 
1047 
1048 int waitDubTarget(ref Terminal t, string target, DubArguments dArgs)
1049 {
1050 	return waitDub(t, dArgs.recipe(buildPath(getBuildTarget(target), "dub.json")));
1051 }
1052 
1053 int waitAndPrint(ref Terminal t, Pid pid)
1054 {
1055 	return wait(pid);
1056 }
1057 
1058 public import std.concurrency;
1059 bool waitOperations(immutable bool delegate()[] operations)
1060 {
1061 	foreach(op; operations)
1062 	{
1063 		spawn((bool delegate() targetOperation)
1064 		{
1065 			ownerTid.send(targetOperation());
1066 		}, op);
1067 	}
1068 
1069 	foreach(i; 0..operations.length)
1070 		if(!receiveOnly!bool) 
1071 			return false;
1072 	return true;
1073 }
1074 
1075 
1076 void putResourcesIn(ref Terminal t, string where)
1077 {
1078 	runEngineDScript(t, "copyresources.d", buildNormalizedPath(configs["gamePath"].str, "assets"), where);
1079 }
1080 
1081 
1082 
1083 string selectInFolder(string selectWhat, string directory, ref Terminal t, ref RealTimeConsoleInput input, 
1084 scope string[] extFilters = [".DS_Store"])
1085 {
1086 	import std.string;
1087 	Choice[] choices;
1088 	LISTING_FILE: foreach(std.file.DirEntry e; std.file.dirEntries(directory, std.file.SpanMode.shallow))
1089 	{
1090 		foreach(f; extFilters)
1091 			if(e.name.endsWith(f)) continue LISTING_FILE;
1092 		choices~= Choice(e.name, null);
1093 	}
1094 	size_t choice;
1095 	choice = selectChoiceBase(t, input, choices, selectWhat);
1096 
1097 	return choices[choice].name;
1098 }
1099 
1100 /** 
1101  * Main difference from selectInFolder is that it returns the choice and also acacepts extra choices.
1102  * Params:
1103  *   selectWhat = Description
1104  *   directory = Directory to iterate
1105  *   t = 
1106  *   input = 
1107  *   extraChoices = May be used to go back or cancel process
1108  * Returns: Selected choice
1109  */
1110 Choice* selectInFolderExtra(string selectWhat, string directory, ref Terminal t, ref RealTimeConsoleInput input,
1111 return scope Choice[] choices, scope Choice[] extraChoices, scope string[] extFilters = [".DS_Store"])
1112 {
1113 	import std.string;
1114 	LISTING_FILES: 
1115 	foreach(std.file.DirEntry e; std.file.dirEntries(directory, std.file.SpanMode.shallow))
1116 	{
1117 		foreach(f; extFilters) if(e.name.endsWith(f)) continue LISTING_FILES;
1118 		choices~= Choice(e.name, null);
1119 	}
1120 	choices = (choices ~ extraChoices).unique;
1121 	size_t choice;
1122 	choice = selectChoiceBase(t, input, choices, selectWhat);
1123 
1124 	return &choices[choice];
1125 }
1126 
1127 
1128 
1129 version(Windows)
1130 {
1131 	import std.windows.registry;
1132 	Key windowsGetKeyWithPath(string[] path...)
1133 	{
1134 		Key hklm = Registry.localMachine;
1135 		if(hklm is null) throw new Error("No HKEY_LOCAL_MACHINE in this system.");
1136 		Key currKey = hklm;
1137 		foreach(p; path)
1138 		{
1139 			try{
1140 				currKey = currKey.getKey(p);
1141 				if(currKey is null) return null;
1142 			}
1143 			catch(Exception e)
1144 			{
1145 				return null;
1146 			}
1147 		}
1148 		return currKey;
1149 	}
1150 }
1151 
1152 string getBuildTarget(string target = __MODULE__)
1153 {
1154 	import std.string:split;
1155 	import std.exception:enforce;
1156 	target = target.split(".")[$-1];
1157 	string path = getHipPath("tools", "build", "targets");
1158 	enforce(std.file.exists(path = buildPath(path, target)), "Target "~target~" does not exists.");
1159 	return path;
1160 }
1161 
1162 void outputTemplate(ref Terminal t, string templatePath)
1163 {
1164 	import template_processor;
1165 	string out_templ;
1166 	
1167 	switch(processTemplate(templatePath, configs["hipremeEnginePath"].str, out_templ, [
1168 		"TARGET_PROJECT": configs["gamePath"].str
1169 	]))
1170 	{
1171 		case TemplateProcessorResult.invalid:
1172 			t.writelnError("Could not process template from path ",templatePath);
1173 			throw new Error("Can't build with invalid template.");
1174 		case TemplateProcessorResult.notFound:
1175 			t.writelnHighlighted("Template at ", templatePath, " not found, your game may use dub.json instead.");
1176 			break;
1177 		default: case TemplateProcessorResult.success:
1178 			t.writelnSuccess("Template at path ", templatePath, " successfully generated");
1179 			std.file.write(buildPath(templatePath, "dub.json"), out_templ);
1180 			break;
1181 	}
1182 }
1183 
1184 void outputTemplateForTarget(ref Terminal t, string target = __MODULE__)
1185 {
1186 	import std.array:split;
1187 	///If it is the default, the target will be "targets.wasm", so, split and get the last.
1188 	string buildTarget = getBuildTarget(target.split(".")[$-1]);
1189 	t.writeln("Regenerating buildscript for target ", buildTarget);
1190 	outputTemplate(t, buildTarget);
1191 }
1192 
1193 void requireConfiguration(string cfgRequired, string purpose, ref Terminal t, ref RealTimeConsoleInput input)
1194 {
1195 	if(!(cfgRequired in configs))
1196 	{
1197 		configs[cfgRequired] = t.getline("Config '"~cfgRequired~"' is required for "~ purpose~ ". \n\tWrite here: ");
1198 		updateConfigFile();
1199 	}
1200 }
1201 
1202 /** 
1203 * 
1204 * Params:
1205 *   original = The original path where the link will redirect
1206 *   link = The path where the link will be created
1207 */
1208 void symlink(string original, string link)
1209 {
1210 	version(Posix){
1211 		std.file.symlink(original, link);
1212 	}
1213 	version(Windows)
1214 	{
1215 		import core.sys.windows.w32api:_WIN32_WINNT;
1216 		static if(_WIN32_WINNT >= 0x600) //WindowsVista or later
1217 		{
1218 			import core.sys.windows.winbase;
1219 			import core.sys.windows.windef:DWORD, MAX_PATH, LPWSTR;
1220 			import std.utf:toUTF16z;
1221 			import std.file:FileException;
1222 
1223 			DWORD typeFlag = 0; //File
1224 			if(std.file.isDir(original))
1225 				typeFlag = SYMBOLIC_LINK_FLAG_DIRECTORY;
1226 			typeFlag|= SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE;
1227 
1228 			if(link.length > MAX_PATH) link = `\\?\`~link;
1229 			if(original.length > MAX_PATH) original = `\\?\`~original;
1230 
1231 			if(!CreateSymbolicLinkW(link.toUTF16z, original.toUTF16z, typeFlag))
1232 			{
1233 				LPWSTR strBuffer;
1234 				DWORD length = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, null, GetLastError(),0, cast(LPWSTR)&strBuffer, 0, null);
1235 				wchar[] str = new wchar[length];
1236 				str[] = strBuffer[0..str.length];
1237 				LocalFree(strBuffer);
1238 				import std.conv;
1239 				throw new FileException(original, str.to!string);
1240 			}
1241 		}
1242 	}
1243 }
1244 
1245 
1246 /** 
1247  * May be used in future. Kept for reference.
1248  */
1249 private bool hasAdminRights()
1250 {
1251 	version(Windows)
1252 	{
1253 		///https://stackoverflow.com/questions/8046097/how-to-check-if-a-process-has-the-administrative-rights
1254 		import core.sys.windows.windows;
1255 		bool hasRights = false;
1256 		HANDLE hToken = NULL;
1257 		if( OpenProcessToken( GetCurrentProcess( ),TOKEN_QUERY,&hToken ) ) {
1258 			TOKEN_ELEVATION Elevation;
1259 			DWORD cbSize = TOKEN_ELEVATION.sizeof;
1260 			if(GetTokenInformation(hToken, TOKEN_INFORMATION_CLASS.TokenElevation, &Elevation, Elevation.sizeof, &cbSize))
1261 				hasRights = Elevation.TokenIsElevated == 1;
1262 		}
1263 		if(hToken) CloseHandle(hToken);
1264 		return hasRights;
1265 	}
1266 	else return false;
1267 }
1268 
1269 
1270 static this()
1271 {
1272 	configs = std.file.exists(getConfigPath) ? Config(parseJSON(std.file.readText(getConfigPath))) : Config(parseJSON("{}"));
1273 	try engineConfig = std.file.exists(getEngineConfigPath) ? parseJSON(std.file.readText(getEngineConfigPath)) : parseJSON("{}");
1274 	catch(Exception e) engineConfig = parseJSON("{}");
1275 }